#!/usr/bin/env python3
#-*- coding:utf-8 -*-
from __future__ import annotations

from http.client import HTTPResponse
from json import dumps, load
from logging import basicConfig, error, exception, info, warning, DEBUG, ERROR, INFO
from logging.config import dictConfig
from os.path import expanduser, join
from sys import argv
from time import sleep
from urllib.request import urlopen, Request
from typing import List, Tuple

from common import create_token
from db.mysql import QueryRunner
from rabbitmq import RabbitMQ

headers = {
	'Authorization': create_token(),
	'Content-Type': 'application/json'
}

class Shipment(object):

	factory_code: str
	shipment_id: int
	db_args: dict

	def __init__(self, factory_code: str, order_no: str, shipment_no: str) -> None:
		self.factory_code = factory_code
		self.db_args = {
			'database': f'lk_{factory_code}',
			'pool_name': f'lake.{factory_code}'
		}
		with QueryRunner('lake.slave', **self.db_args) as runner:
			result = runner.fetchone('SELECT `id` FROM `opr_shipment` WHERE `order_no` = %s AND `ch_shipment_no` = %s', order_no, shipment_no)
			if result:
				self.shipment_id = int(result[0])
			else:
				raise ValueError(f"Shipment {shipment_no} NOT Found")

		info('Running %s %s %s in %s', self.shipment_id, shipment_no, order_no, self.factory_code)

	def _get_shipment_status(self) -> int | None:
		with QueryRunner('lake.slave', **self.db_args) as runner:
			result = runner.fetchone('SELECT `status` FROM `opr_shipment` WHERE `id` = %s', self.shipment_id)
			return int(result[0]) if result else None

	def _get_product_status(self, barcode: str) -> int | None:
		with QueryRunner('lake.slave', **self.db_args) as runner:
			result = runner.fetchone('SELECT `status` FROM `opr_product` WHERE `barcode` = %s', barcode)
			return int(result[0]) if result else None

	def _get_companion(self, barcode: str) -> str | None:
		with QueryRunner('lake.slave', **self.db_args) as runner:
			result = runner.fetchone('SELECT p1.`barcode` FROM `opr_product` p1 JOIN `opr_product` p2 ON p1.`companion_id` = p2.`id` WHERE p2.`barcode` = %s', barcode)
			return result[0] if result else None

	def _get_express_no(self) -> str | None:
		with QueryRunner('lake.slave', **self.db_args) as runner:
			result = runner.fetchone('SELECT `express_no` FROM `opr_shipment` WHERE `id` = %s', self.shipment_id)
			return result[0] if result else None

	def _http_request(self, url: str, method: str, data: dict | None = None) -> None:
		info(url)
		if data:
			post: str = dumps(data, ensure_ascii=False)
			info(post)
			request: Request = Request(url, headers=headers, method=method.upper(), data=post.encode())
		else:
			request: Request = Request(url, headers=headers, method=method.upper())

		response: HTTPResponse = urlopen(request)
		json = load(response)
		code: int = int(json.get('code'))
		if code < 300:
			info('[%s %s] %s', response.status, response.reason, json.get('code'))
		else:
			info('[%s %s] %s', response.status, response.reason, dumps(json, ensure_ascii=False))

	def _get(self, url: str) -> None:
		self._http_request(url, 'GET')

	def _post(self, url: str, data: dict | None = None) -> None:
		self._http_request(url, 'POST', data)

	def _run_qc(self, barcode: str) -> int:
		companion: str = self._get_companion(barcode)
		self._get(f'https://{self.factory_code}.haishenxiyi.net/api/product/{barcode}')
		if not companion:
			self._post(f'https://{self.factory_code}.haishenxiyi.net/api/product/{barcode}/qualified')
		else:
			self._get(f'https://{self.factory_code}.haishenxiyi.net/api/product/{companion}')
			self._post(f'https://{self.factory_code}.haishenxiyi.net/api/product/{barcode}/{companion}/qualified')
		return self._get_product_status(barcode)

	def _run_attachment(self, barcode: str) -> int:
		with QueryRunner('lake.slave', **self.db_args) as runner:
			result = runner.fetchall('SELECT a.`barcode` FROM `opr_attachment` a JOIN `opr_product` p ON a.`product_id` = p.`id` WHERE p.`barcode` = %s AND a.`flag` & 512 != 512', barcode)
		for att_barcode, in result:
			self._post(f'https://{self.factory_code}.haishenxiyi.net/api/product/{barcode}/attachment/{att_barcode}/scan')
		return self._get_product_status(barcode)

	def _run_wrap(self, barcode: str) -> int:
		self._get(f'https://{self.factory_code}.haishenxiyi.net/api/product/wrap/{barcode}')
		self._get(f'https://{self.factory_code}.haishenxiyi.net/api/product/checkLable/{barcode}')
		return self._get_product_status(barcode)

	def _run_place(self, barcode: str) -> int:
		# TODO
		return self._get_product_status(barcode)

	def _run_print(self) -> int:
		self._post(f'https://{self.factory_code}.haishenxiyi.net/api/shipment/picking', [ self.shipment_id ])
		return self._get_shipment_status()

	def _run_picking(self, barcode: str) -> int:
		#TODO
		return self._get_product_status(barcode)

	def _run_to_placed(self) -> int:
		with QueryRunner('lake.slave', **self.db_args) as runner:
			rows: List[Tuple[str, str]] = runner.fetchall('SELECT p.`barcode`, p.`status` FROM `opr_product` p JOIN `opr_shipment` s ON p.`shipment_id` = s.`id` WHERE s.`id` = %s AND s.`flag` & 512 != 512 AND p.`companion_id` IS NULL AND p.`flag` & 512 != 512', self.shipment_id)

		if not rows:
			warning('NO product found in shipment %s', self.shipment_id)
			# TODO cancel shipment?
		else:
			for barcode, status in rows:
				'''
				VERIFY("1", "核查中"),
				TOWASH("2", "待洗涤"),
				WASHING("3", "洗涤中"),
				WASHED("4", "已洗涤"),
				TOINSPECT("5", "待质检"),
				ATTACH("6", "配附件"),
				TOWRAP("7", "待包装"),
				WRAPPED("8", "已包装"),
				PLACED("9", "已上架"),
				TOPICK("10", "待拣货"),
				PICKED("11", "已拣货"),
				PACKED("12", "已装箱"),
				SHIPPED("13", "已出厂");
				'''
				status = int(status)
				count = 0
				while status < 9:
					if count >= 10:
						raise RecursionError(f"Product {barcode} NOT shipped after {count} iteration")

					info('%s -> %s', barcode, status)
					if status <= 5:
						status = self._run_qc(barcode)
					elif status == 6:
						status = self._run_attachment(barcode)
					elif status == 7:
						status = self._run_wrap(barcode)
					elif status == 8:
						status = self._run_place(barcode)
					count += 1
				info('%s -> %s', barcode, status)
		return self._get_shipment_status()

	def _run_package(self) -> int:
		with QueryRunner('lake.slave', **self.db_args) as runner:
			barcodes = runner.fetchall('SELECT `barcode` FROM `opr_product` WHERE `shipment_id` = %s AND `companion_id` IS NULL AND `flag` & 512 != 512', self.shipment_id)

		self._get(f'https://{self.factory_code}.haishenxiyi.net/api/product/package/{barcodes[0][0]}/detail')
		for barcode, in barcodes:
			self._post(f'https://{self.factory_code}.haishenxiyi.net/api/product/package/{barcode}/scan')

		express_no: str = self._get_express_no()
		self._post(f'https://{self.factory_code}.haishenxiyi.net/api/product/label/{express_no}/scan')

		return self._get_shipment_status()

	def _run_ship(self) -> int:
		express_no: str = self._get_express_no()
		self._post(f'https://{self.factory_code}.haishenxiyi.net/api/product/outFactory/{express_no}')
		return self._get_shipment_status()

	def _wait_for_express(self) -> int:
		count = 0
		while not self._get_express_no():
			info('Waiting for express')
			count += 1
			if count >= 30:
				raise TimeoutError(f"No express after {count} seconds")
			sleep(1)
		return self._get_shipment_status()

	def run(self) -> None:
		'''
		PENDING("1", "等待中"),
		ORDERING("2", "物流下单"),
		TOPICK("3", "待拣货"),
		PICKING("4", "拣货中"),
		PICKED("5", "已拣货"),
		PACKING("6", "装箱中"),
		PACKED("7", "已装箱"),
		SHIPPED("8", "已出厂");
		'''
		status: int = self._get_shipment_status()
		count: int = 0
		while status < 8:
			if count >= 10:
				raise RecursionError(f"Shipment NOT shipped after {count} iteration")
			info('Shipment status %s', status)

			if status == 1:
				status = self._run_to_placed()
			elif status == 2:
				status = self._wait_for_express()
			elif status == 3:
				express_no: str = self._get_express_no()
				if not express_no:
					raise ValueError(f"Shipment status 3 without express no")

				with RabbitMQ('waterfall') as rabbit:
					rabbit.send(
						message={
							'no': express_no,
							'carrier': express_no[:2]
						},
						routing_key='express.order.cancel'
					)
				status = self._run_print()
			elif status < 7:
				status = self._run_package()
			elif status == 7:
				status = self._run_ship()

			count += 1

		if status == 8:
			express_no: str = self._get_express_no()
			if not express_no:
				raise ValueError(f"Shipment status 8 without express no")

			with RabbitMQ('waterfall') as rabbit:
				rabbit.send(
					message={
						'no': express_no,
						'carrier': express_no[:2],
						'status': 'DELIVERED'
					},
					exchange='express.parcel.status'
				)

def get_shipments(order_no: str) -> List[Tuple[str, str]]:
	shipments = []
	sql: str = 'SELECT f.`code`, s.`no` FROM `ffl_shipment` s JOIN `fty_factory` f ON s.`factory_id` = f.`id` WHERE s.`order_no` = %s AND s.`flag` & 16 = 0'
	for db in ('wf_factory', 'pn_factory'):
		with QueryRunner('waterfall.ro', database=db, pool_name=db) as runner:
			rows = runner.fetchall(sql, order_no)
			if rows:
				shipments += rows
	return shipments

def run_order(order_no: str) -> None:
	for factory_code, shipment_no in get_shipments(order_no):
		try:
			shipment: Shipment = Shipment(factory_code, order_no, shipment_no)
			shipment.run()
		except Exception as e:
			exception(*e.args)

if __name__ == '__main__':
	basicConfig(
		format='%(asctime)s %(name)s %(levelname)s %(message)s',
		#filename=argv[0].rsplit('.', 1)[0] + '.log',
		level=INFO
	)
	dictConfig({
		'version': 1,
		'loggers': {
			'db.mysql': {
				'level': ERROR
			}
		}
	})

	if len(argv) > 1:
		for order_no in argv[1:]:
			run_order(order_no)
	else:
		home_dir = expanduser("~")
		file_path = join(home_dir, 'Downloads', 'order.csv')

		with open(file_path, 'r') as file:
			for line in file:
				order_no: str = line.strip()
				if order_no:
					run_order(order_no)
